iT邦幫忙

2024 iThome 鐵人賽

DAY 28
0

在我們之前的應用中,已經介紹了如何使用 JWT 來進行身份驗證,但我們沒有處理不同角色之間的權限問題。這次,我們將進一步擴展,實作基於角色的權限管理,讓應用可以根據用戶的角色與權限來決定他們可以進入哪些頁面、操作哪些功能。

在這篇文章中,我們會教你如何使用 Role 和 Permissions 來管控用戶的操作權限,並且利用這些權限來改寫我們之前的 Route Guard,實現一個更完整的權限控制系統。

https://ithelp.ithome.com.tw/upload/images/20241006/20168326t7ulYwXI6Y.png

文章大綱:

  1. 什麼是 Role-Based Permissions?
  2. 資料庫設計:使用 Migration 和 Seeder 來處理角色與權限
  3. 登入後如何處理角色與權限
  4. 在前端使用權限來控制操作
  5. 使用角色與權限改寫 Route Guard
  6. 角色與權限管理系統的彈性與擴充性

1. 什麼是 Role-Based Permissions?

在系統中,不同的使用者會有不同的身份,像是管理員、一般使用者等,而不同的身份往往會有不同的操作權限。Role-Based Permissions 就是這樣一套權限管理系統,讓我們可以根據使用者的角色,來決定他們是否有權限進行某些操作。

  • Role(角色):這代表使用者的身份,如 "admin"、"user" 等。
  • Permission(權限):這代表具體的操作,如 "view_products"(查看商品)、"edit_products"(編輯商品)。
  • Role-Permission 關聯表:用來定義每個角色具備的許可。

範例:購物網站中的角色與權限

Role Permission
admin view_products, edit_products
user view_products

透過這樣的架構,我們就能讓 admin 角色擁有查看和編輯商品的權限,而 user 角色則只能查看商品,無法進行編輯操作。


2. 資料庫設計:使用 Migration 和 Seeder 來處理角色與權限

為了幫助讀者更容易理解,接下來會使用 Node.js + Express 搭配 Sequelize 來實作資料庫的角色與權限結構,並通過 Migration 和 Seeder 的方式來設置與填充資料。這樣不需要手動寫 SQL,可以透過工具自動生成資料庫表結構與範例數據。

2-1. 使用 Sequelize 定義資料庫結構

首先,我們會使用 Sequelize Migration 來定義三張表:Roles、Permissions 和 Role_Permission。這三張表格可以有效管理角色與權限的關聯。

2-1-1. 創建 Sequelize Migration

假設你已經安裝了 Sequelize,並且專案內有 Sequelize CLI,接下來我們會創建三個資料表:


npx sequelize-cli migration:generate --name create-roles-table
npx sequelize-cli migration:generate --name create-permissions-table
npx sequelize-cli migration:generate --name create-role-permission-table

2-1-2. 編輯 Migrations 檔案

Roles Table Migration:

migrations 資料夾中,找到剛剛生成的 create-roles-table 檔案,並加入以下程式碼:

'use strict';
module.exports = {
  up: async (queryInterface, Sequelize) => {
    await queryInterface.createTable('Roles', {
      id: {
        allowNull: false,
        autoIncrement: true,
        primaryKey: true,
        type: Sequelize.INTEGER
      },
      role: {
        type: Sequelize.STRING,
        allowNull: false
      },
      createdAt: {
        allowNull: false,
        type: Sequelize.DATE
      },
      updatedAt: {
        allowNull: false,
        type: Sequelize.DATE
      }
    });
  },
  down: async (queryInterface, Sequelize) => {
    await queryInterface.dropTable('Roles');
  }
};

Permissions Table Migration:

create-permissions-table 檔案中,加入以下程式碼:

'use strict';
module.exports = {
  up: async (queryInterface, Sequelize) => {
    await queryInterface.createTable('Permissions', {
      id: {
        allowNull: false,
        autoIncrement: true,
        primaryKey: true,
        type: Sequelize.INTEGER
      },
      permission: {
        type: Sequelize.STRING,
        allowNull: false
      },
      createdAt: {
        allowNull: false,
        type: Sequelize.DATE
      },
      updatedAt: {
        allowNull: false,
        type: Sequelize.DATE
      }
    });
  },
  down: async (queryInterface, Sequelize) => {
    await queryInterface.dropTable('Permissions');
  }
};

Role_Permission Table Migration:

create-role-permission-table 檔案中,加入以下程式碼,建立 RolesPermissions 之間的關聯:

'use strict';
module.exports = {
  up: async (queryInterface, Sequelize) => {
    await queryInterface.createTable('Role_Permission', {
      role_id: {
        type: Sequelize.INTEGER,
        references: {
          model: 'Roles',
          key: 'id'
        },
        onUpdate: 'CASCADE',
        onDelete: 'CASCADE'
      },
      permission_id: {
        type: Sequelize.INTEGER,
        references: {
          model: 'Permissions',
          key: 'id'
        },
        onUpdate: 'CASCADE',
        onDelete: 'CASCADE'
      },
      createdAt: {
        allowNull: false,
        type: Sequelize.DATE
      },
      updatedAt: {
        allowNull: false,
        type: Sequelize.DATE
      }
    });
  },
  down: async (queryInterface, Sequelize) => {
    await queryInterface.dropTable('Role_Permission');
  }
};

2-1-3. 執行 Migration

在寫好 migration 檔案後,執行以下指令來建立資料表:

npx sequelize-cli db:migrate

這樣,資料庫就會自動創建三張表:RolesPermissionsRole_Permission,用來存放角色和權限的相關資料。

2-2. 使用 Seeder 來填充資料

接下來,使用 Sequelize Seeder 來填充一些初始角色和權限數據,讓系統可以開始使用這些資料。

2-2-1. 生成 Seeder 檔案

首先,生成 Seeder 檔案:

npx sequelize-cli seed:generate --name add-roles
npx sequelize-cli seed:generate --name add-permissions
npx sequelize-cli seed:generate --name add-role-permission

2-2-2. 編輯 Seeder 檔案

Roles Seeder:

add-roles 檔案中,加入以下程式碼來插入一些預設角色:

'use strict';

module.exports = {
  up: async (queryInterface, Sequelize) => {
    return queryInterface.bulkInsert('Roles', [
      { role: 'admin', createdAt: new Date(), updatedAt: new Date() },
      { role: 'user', createdAt: new Date(), updatedAt: new Date() },
    ]);
  },
  down: async (queryInterface, Sequelize) => {
    return queryInterface.bulkDelete('Roles', null, {});
  }
};

Permissions Seeder:

add-permissions 檔案中,加入以下程式碼來插入一些預設權限:

'use strict';

module.exports = {
  up: async (queryInterface, Sequelize) => {
    return queryInterface.bulkInsert('Permissions', [
      { permission: 'view_products', createdAt: new Date(), updatedAt: new Date() },
      { permission: 'edit_products', createdAt: new Date(), updatedAt: new Date() },
    ]);
  },
  down: async (queryInterface, Sequelize) => {
    return queryInterface.bulkDelete('Permissions', null, {});
  }
};

Role_Permission Seeder:

add-role-permission 檔案中,插入角色和權限的關聯:

'use strict';

module.exports = {
  up: async (queryInterface, Sequelize) => {
    return queryInterface.bulkInsert('Role_Permission', [
      { role_id: 1, permission_id: 1, createdAt: new Date(), updatedAt: new Date() }, // admin can view products
      { role_id: 1, permission_id: 2, createdAt: new Date(), updatedAt: new Date() }, // admin can edit products
      { role_id: 2, permission_id: 1, createdAt: new Date(), updatedAt: new Date() }, // user can view products
    ]);
  },
  down: async (queryInterface, Sequelize) => {
    return queryInterface.bulkDelete('Role_Permission', null, {});
  }
};

2-3. 執行 Seeder

寫好 Seeder 後,執行以下指令來填充資料:

npx sequelize-cli db:seed:all

這樣,資料庫就會自動填充角色、權限和它們的關聯資料。

Roles、Permissions 和 Role_Permission 之間的關聯

Roles 表

這張表記錄了系統中的角色,例如管理員和一般使用者。

https://ithelp.ithome.com.tw/upload/images/20241006/20168326nNiz662NKt.png

Permissions 表

這張表記錄了系統中不同的操作權限,例如查看產品和編輯產品。

https://ithelp.ithome.com.tw/upload/images/20241006/201683264uxQhCI2tt.png

Role_Permission 表

這張表是關聯表,用來表示每個角色具備的權限。在這裡,我們可以看到每個角色擁有的具體權限:

https://ithelp.ithome.com.tw/upload/images/20241006/20168326m5qReGPAv4.png

表格關係解釋

  • Roles 表admin(角色 id = 1)和 user(角色 id = 2)。
  • Permissions 表view_products(權限 id = 1)和 edit_products(權限 id = 2)。
  • Role_Permission 表:表示 admin 角色擁有查看產品和編輯產品的權限,而 user 角色只能查看產品,沒有編輯產品的權限。

這三張表的關係如下:

  • admin 角色對應兩個權限:view_productsedit_products
  • user 角色對應一個權限:view_products

3. 登入後如何處理角色與權限

當使用者登入成功後,除了給他們一個 JWT Token,我們還需要根據他們的角色來決定他們的操作權限。以下是我們的完整流程:

3-1. 登入後取得 JWT

當用戶登入成功時,後端會生成一個 JWT,這個 Token 包含使用者的基本身份資訊(例如 userId),並回傳給前端:

const token = jwt.sign({ userId }, process.env.JWT_SECRET, { expiresIn: '1h' });
res.json({ token });

3-2. 查詢角色與權限

當需要操作一些敏感功能(如編輯商品)時,前端會向後端請求使用者的權限,後端則會查詢使用者的角色,並從 Role_Permission 關聯表裡找到這個角色對應的權限。

// controllers/permissionController.js
exports.getPermissions = async (req, res) => {
  const userId = req.userId; // 從 JWT 取得 userId

  // 查詢使用者角色與對應的權限
  const userRole = await db.User.findOne({ where: { id: userId }, include: ['role'] });
  const permissions = await db.Role_Permission.findAll({ where: { role_id: userRole.role.id } });

  res.json({ permissions });
};

4. 在前端使用權限來控制操作

現在我們已經知道如何在後端查詢使用者的權限,那前端該如何使用這些權限來控制頁面顯示呢?例如,當使用者想要進入編輯商品頁面時,我們可以先請求權限資料,再根據權限決定是否允許存取。

範例:檢查用戶權限

this.permissionService.getPermissions().subscribe((permissions) => {
  if (!permissions.includes('edit_products')) {
    alert('您沒有權限編輯商品');
    this.router.navigate(['/home']);
  }
});

這段程式碼會在使用者進入頁面時,向後端請求他的權限,並檢查是否有 edit_products 的權限,沒有的話就重導到首頁。


5. 使用角色與權限改寫 Route Guard

在我們的應用中,我們可以用 Role-Permission 的架構來改寫之前的 Route Guard,讓我們可以根據使用者的角色與權限來決定是否允許他們訪問某些頁面。

5-1. 改寫 auth.guard.ts

在之前的 auth.guard.ts 中,我們是根據是否有 JWT 來決定是否允許訪問。現在我們要進一步檢查用戶的權限。這樣,我們可以根據權限決定某些頁面(如管理員頁面)是否允許進入。

檔案路徑:src/app/guards/auth.guard.ts

import { Injectable } from '@angular/core';
import { CanActivate, Router } from '@angular/router';
import { AuthService } from '../services/auth.service'; // 確保路徑正確
import { PermissionService } from '../services/permission.service';

@Injectable({
  providedIn: 'root',
})
export class AuthGuard implements CanActivate {
  constructor(
    private authService: AuthService,
    private permissionService: PermissionService,
    private router: Router
  ) {}

  canActivate(): boolean {
    const token = localStorage.getItem('token'); // 取得 JWT Token
    if (token) {
      return this.checkPermissions(); // 確認權限
    } else {
      this.router.navigate(['/login']); // 未登入,重導至登入頁
      return false;
    }
  }

  private checkPermissions(): boolean {
    // 假設這個 Guard 用於管理員頁面
    this.permissionService.getPermissions().subscribe((permissions) => {
      if (permissions.includes('admin_access')) {
        return true; // 有權限,允許訪問
      } else {
        this.router.navigate(['/home']); // 沒有權限,重導至首頁
        return false;
      }
    });
  }
}

5-2. 在路由中使用 Role-Based Guard

接下來,我們將這個改寫過的 Route Guard 套用到路由上,來保護特定的頁面,例如管理頁面只能由有權限的使用者訪問。

檔案路徑:src/app/app-routing.module.ts

import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { AdminComponent } from './admin/admin.component';
import { HomeComponent } from './home/home.component';
import { AuthGuard } from './guards/auth.guard';

const routes: Routes = [
  { path: 'home', component: HomeComponent },
  { path: 'admin', component: AdminComponent, canActivate: [AuthGuard] }, // 受保護的管理員頁面
  { path: '', redirectTo: '/home', pathMatch: 'full' },
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule],
})
export class AppRoutingModule {}

6. 角色與權限管理系統的彈性與擴充性

我們已經完成了角色與權限管理系統的基本架構,並且進一步探討了如何讓這套系統具備彈性,動態調整角色的權限,並在前端根據這些權限動態渲染頁面。這樣不僅提升了系統的安全性,也讓整個應用變得更具彈性與可維護性。

透過這樣的架構,你可以輕鬆管理大型應用中的角色和權限,並且確保不同角色的用戶都能看到他們應該看到的內容,不會造成混亂或安全問題。

這就是一個完整的角色與權限管理系統,讓你的應用程式從登入、操作、權限控管到頁面呈現,都變得既安全又靈活!


上一篇
Day27 密碼驗證流程 – MySQL 中的密碼儲存與檢查
下一篇
Day29 夥伴的羈絆,走出孤獨程式戰士的 Side Project 之路
系列文
從零開始:全端新手的困境與成長30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言